import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.io.Writer;

/*
 * Справочник ФНСИ.
 * Хранит данные в 2-х AVTree - data и index
 * data - основное дерево, где хранится тройка: ключ, основное значение и дополнительное значени. Это Word и RefData.
 */
final public class Ref {

    private static ArrayList<Node<RefData>> EMPTY_RESULT = new ArrayList<Node<RefData>>(0);

    String binfilename = null;
    volatile boolean passportLoaded = false;
    /*
     * для работы Like2 надо чтобы вершины data были пронумерованы, то есть чтобы
     * был присвоен id каждой вершине. Это происходит при сохранении в файл или
     * при загрузке из JSON внутреннего формата или из бинарного файла.
     */
    volatile boolean filterLike2Ready = false;

    // данные паспорта, заполняются из таблицы fnsi_ref базы SQLite3
    String codeSystem;
    String version;
    String[] codeSystemOther;
    String codeSystemName;
    int rowsCount;
    String keyField; // поле, которое в настройках указано как ключ справочника (атрибут code). Если
                     // null, то имя поля с ключом будет взят из паспорта
    String valueField; // поле, которое в настройках указано как значение справочника (атрибут
                       // displayName). Если null, то имя поля со значением будет взято из паспорта

    // настройки справочников - таблица ref_setting
    String[] fieldNames; // если не null, то будут сохраняться значения всех полей справочника. Иначе
                         // только то, что указано в valueField.
    boolean load; // загружать или нет справочник
    boolean validateRefName; // выполнять ли по нему проверку codeSystemName в СЭМД
    boolean validateValue; // выполнять ли по нему проверку displayName в СЭМД

    // Набор курсоров для постраничного считывания данных
    ConcurrentHashMap<String, AVLTree<Iterable<Node<RefData>>>.LikeIterator> filterLikeCursor = new ConcurrentHashMap<String, AVLTree<Iterable<Node<RefData>>>.LikeIterator>(
            100);

    ConcurrentHashMap<String, AVLTree<Iterable<Node<RefData>>>.LikeIterator[]> filterLike2Cursors = new ConcurrentHashMap<String, AVLTree<Iterable<Node<RefData>>>.LikeIterator[]>(
            100);
    ConcurrentHashMap<String, Integer> filterLike2CursorsPosition = new ConcurrentHashMap<String, Integer>(100);

    ConcurrentHashMap<String, AVLTree<RefData>.RefIterator> dataCursor = new ConcurrentHashMap<String, AVLTree<RefData>.RefIterator>(
            100);

    // данные справочника, загруженные из ФНСИ.
    AVLTree<RefData> data = new AVLTree<RefData>() {
        final public RefData loadValue(final TreeReader<RefData> parser) {
            ArrayList<Object> d = parser.parseDataArray();
            Object[] value = (Object[]) d.get(5);
            Object[] extdata = (Object[]) d.get(6);
            Array<WordInterface> ar = new Array<WordInterface>(value);
            for (int i = 0; i < value.length; i++) {
                value[i] = dict.getNodeById((Integer) value[i]).value;
            }
            return new RefData(ar, extdata);
        }

        final public Node<RefData> newNode(Word key, RefData value) {
            return new NodeDataExt<RefData>(key, value);
        }

        final public Node<RefData> newNode(Word key, RefData value, final TreeReader<RefData> reader) {
            return new Node<RefData>(key, value);
        }
    };

    // Словарь, из которого собираются предложения в справочниках ФНСИ (поле Value)
    AVLTree<WordInterface> dict = new AVLTree<WordInterface>() {
        final public WordInterface loadValue(final TreeReader<WordInterface> parser) {
            ArrayList<Object> data = parser.parseWord();
            return (WordInterface) data.get(5);
        }

        final public Node<WordInterface> newNode(Word key, WordInterface value) {
            return new Node<WordInterface>(key, value);
        }

        // если надо доп данные загрузить из файла, например, height, то это можно
        // сделать здесь
        final public Node<WordInterface> newNode(Word key, WordInterface value,
                final TreeReader<WordInterface> parser) {
            return new Node<WordInterface>(key, value);
        }
    };

    /*
     * Индекс - массив нод дерева data, где встречается ключ
     * В поле value содержится 2 типа - HashSet и Array. Первыйиспользуется в
     * процессе построения индекса, но по завершению построения заменяется на Array
     */
    AVLTree<Iterable<Node<RefData>>> index = new AVLTree<Iterable<Node<RefData>>>() {
        final public Iterable<Node<RefData>> loadValue(final TreeReader<Iterable<Node<RefData>>> parser) {
            ArrayList<Object> d = parser.parseIndexArray();
            Object[] value = (Object[]) d.get(5);
            Array<Node<RefData>> ar = new Array<Node<RefData>>(value);
            for (int i = 0; i < value.length; i++) {
                value[i] = data.getNodeById((Integer) value[i]);
            }
            return ar;
        }

        final public Node<Iterable<Node<RefData>>> newNode(Word key, Iterable<Node<RefData>> value) {
            return new Node<Iterable<Node<RefData>>>(key, value);
        }

        final public Node<Iterable<Node<RefData>>> newNode(Word key, Iterable<Node<RefData>> value,
                final TreeReader<Iterable<Node<RefData>>> parser) {
            return new Node<Iterable<Node<RefData>>>(key, value);
        }
    };
    private WordIndex.AVLTreePositionComparator indexHeightComparator = new WordIndex.AVLTreePositionComparator(index);

    /**
     * @param oid - ОИД справочника
     * @param ver - версия справочника
     */
    public Ref(final String oid, final String ver) {
        this.codeSystem = oid;
        this.version = ver;
    }

    public boolean isEmpty() {
        return data.root == null;
    }

    /**
     * Построение справочника:
     * 1. Все строки разбиваем на слова.
     * 2. Составляем словарь всех слов - dict (AVLTree). В ключе - слово в win1251,
     * в значении - Слово.
     * 
     * Слово - это одно из 3-х:
     * а) слово в win1251 если конвертируется без потерь - class Word
     * б) слово в Unicode иначе - class WordUnicode
     * в) пробел - class Space
     * Слова регистрозависимые, тк из слов надо восстановить исходные записи
     * справочника ФНСИ (поле Value)
     * 
     * 3. Строится основая БД - AVLTree data. В ключе - код записи в справочнике
     * ФНСИ, в значении - массив Слов, прочем слова - ссылки на dict
     * 4. Строится index - AVLTree, где в ключе слова в win1251, в значении - массив
     * узлов дерева data, где встречается это слово.
     * 5. Дополнительные сведения справочника (содержимое полей, перечисленных в
     * REKV FNSIlist.json проекта mzrepos) хранятся в конце файла. При чтении дерева
     * загружаются только индексы данных в конце файла. Данные загружаются только
     * при обращении к ним.
     * 
     * @param a массив из 3х элементов - код (String), значение (String), доп данные
     *          (LinkedList<String>). Состав данных соответветствует списку реквизитов
     *          REKV в FNSIlist.json
     */
    public void addRecord(final Object[] a) {
        Object k_ = a[0];
        Object v_ = a[1];
        /*
         * элементы справочника где значение ключа равно null игнорируем. В ФНСИ такое
         * встречается на помеченных на удаление.
         * value может быть null, если в некорректно определилось поле со значением (по
         * паспорту или по файлу настроек)
         */
        if (k_ == null || v_ == null)
            return;

        final Word key = new Word((String) k_);
        final ArrayList<WordInterface> words = Word.split((String) v_);
        final ArrayList<WordInterface> words2 = new ArrayList<WordInterface>(words.size());
        for (int i = 0; i < words.size(); i++) {
            final Word k;
            WordInterface part = words.get(i);
            if (part instanceof WordUnicode) { // Шаг 1. помещаем слово справочника в словарь
                k = new Word((WordUnicode) part);
                if (k.toString().equals(part.toString())) { // проверка корректности перевода в 1251
                    part = k;
                }
            } else if (part instanceof Space) {
                k = new Word(part.toString());
            } else {
                throw new UnsupportedOperationException();
            }
            Node<WordInterface> k2 = dict.get(k);
            if (k2 == null) {
                dict.put(k, part);
            } else {
                part = k2.value;
            }
            words2.add(part);
        }
        @SuppressWarnings("unchecked")
        LinkedList<String> extradata = (LinkedList<String>) a[2];
        String[] ar = null;
        if (extradata.size() != 0) {
            ar = new String[extradata.size()];
            extradata.toArray(ar);
        }
        Node<RefData> node = data.get(key);
        if (node == null) {         // в некоторых справочниках есть дубли. Например, НМУ версии 2.2
            data.put(key, new RefData(words2, ar));
            node = data.get(key);
        }

        // обновляем индекс
        for (int i = 0; i < words.size(); i++) {
            final WordInterface part = words.get(i);
            final WordIndex k;
            if (part instanceof WordUnicode) {
                k = new WordIndex((WordUnicode) part, true);
            } else { // пробелы пропускаем
                continue;
            }
            Node<Iterable<Node<RefData>>> tnode = index.get(k);
            Iterable<Node<RefData>> to_update = (tnode != null) ? tnode.value : null;
            if (to_update == null) {
                to_update = new HashSet<Node<RefData>>();
                index.put(k, to_update);
            }
            // в одном предложении может быть несколько одинаковых слов.
            // В этом случае последующая строка даст дубли в индексе.
            // Поэтому HashSet, который ниже будет преобразован в Array
            ((Set<Node<RefData>>) to_update).add(node);
        }
    }

    /**
     * упаковываем дерево индекса - меняем HashSet на сортированный Array
     * Вызов метода необходим перед сохранением индекса в файлы, чтобы
     * все массивы ссылок на data в индексе был сортированными. Сортировка
     * требуется для работы алгоритма Like2.
     */
    private void repackIndex() {
        if (!isEmpty()) {
            AVLTree<Iterable<Node<RefData>>>.RefIterator iter = index.new RefIterator();
            while (iter.hasNext()) {
                Node<Iterable<Node<RefData>>> node = iter.next();
                List<Node<RefData>> ar = new ArrayList<Node<RefData>>(
                        (Set<Node<RefData>>) node.value);
                Collections.sort(ar);
                node.value = ar;
            }
        }
    }

    /**
     * Поиск filterKey - получение записи справочнике ФНСИ по key.
     * Всегда возвращается одна запись.
     * 
     * @key - ключ записи справочника ФНСИ
     */
    public JSONObject filterKey(final String key) throws IOException {
        if (isEmpty()) {
            return toJson(EMPTY_RESULT, false, null);
        }

        ArrayList<Node<RefData>> searchResult = new ArrayList<Node<RefData>>(1);
        Node<RefData> r = (Node<RefData>) data.get(key);
        if (r != null)
            searchResult.add(r);
        return toJson(searchResult, false, null);
    }

    /**
     * Поиск data - получение всех записей справочнике ФНСИ по Value.
     * Результаты всегда сортированы по увеличению значения ключа.
     * 
     * В каждой порции данных указывается количество записей в справочнике.
     *
     * При выполнении запроса используется логика СУБД - при первом вызове data
     * формируется курсор.
     * Последующие вызовы data - аналог fetch у курсора. Курсор идентифируется по id
     * Выборка данных завершилась тогда, когда в объекте hasNext установлено false.
     * 
     * @id - идентфикатор запроса. Каждый запрос должен иметь уникальный
     *     идентификатор, который надо сформировать из URL запроса
     * 
     * @results_per_page - сколько записей вернуть
     */
    public JSONObject data(final String id, final int results_per_page) throws IOException {
        if (isEmpty()) {
            return toJson(EMPTY_RESULT, false, null);
        }

        ArrayList<Node<RefData>> searchResult = new ArrayList<Node<RefData>>(results_per_page);
        AVLTree<RefData>.RefIterator cursor = dataCursor.get(id);
        boolean from_store = true;
        if (cursor == null) {
            cursor = data.new RefIterator();
            from_store = false;
        }
        while (cursor.hasNext()) {
            Node<RefData> res = (Node<RefData>) cursor.next();
            searchResult.add(res);
            if (searchResult.size() == results_per_page)
                break;
        }
        if (cursor.hasNext())
            dataCursor.put(id, cursor);
        else if (from_store)
            dataCursor.remove(id);

        return toJson(searchResult, cursor.hasNext(), null);
    }

    /**
     * Поиск filterLike - поиск записей справочнике ФНСИ по Value. Запись выдается
     * поиском если в Value входит искомая подстрока (без учета регистра)
     * 
     * Алгоритм поиска:
     * 1. Шаблон поиск разбивается на слова. Слова записывают в class Word - всегда
     * в 1251.
     * 2. В index выполняется поиск всех key (без учета регистра) по наибольшему
     * слову из шаблона. Дерево index построено морфологически, то есть без учета
     * окончаний.
     * 3. Из index берутся массивы ссылок на записи data, преобразуется в 1251 и
     * выполняется дофильтрация по полному шаблону (без учета регистра, с учетом
     * морфлогии, т.е. окончаний)
     * 
     * Пример:
     * Шаблон 'медицинская сес'
     * 
     * Данные:
     * 'медицинская сестра' - соответствует
     * 'сестра медицинская' - не соответствует
     * 
     * Пример:
     * Шаблон 'едицинск'
     * 
     * Данные:
     * 'медицинская сестра' - соответствует
     * 
     * В SQL ближайший аналог - "LIKE '%pattern%"
     * 
     * При выполнении запроса используется логика СУБД - при первом вызове
     * filterLike формируется курсор.
     * Последующие вызовы filterLike - аналог fetch у курсора. Курсор идентифируется
     * по id
     * В каждой порции данных указывается количество записей в порции.
     * Выборка данных завершилась тогда, когда в объекте hasNext установлено false.
     * 
     * @id - идентфикатор запроса. Каждый запрос должен иметь уникальный
     *     идентификатор, который надо сформировать из URL запроса
     * 
     * @pattern - шаблон поиска
     * 
     * @results_per_page - сколько записей вернуть
     */
    public JSONObject filterLike(final String reqid, final String pattern_text, final int results_per_page)
            throws IOException {
        if (isEmpty()) {
            return toJson(EMPTY_RESULT, false, null);
        }

        AVLTree<Iterable<Node<RefData>>>.LikeIterator cursor = filterLikeCursor.get(reqid);
        boolean from_store = true;
        if (cursor == null) {
            /*
             * Данные для алгоритма для алгоритма Бойера—Мура
             * Если исходный паттерн содержит пробелы, то он разбивается на слова.
             * Сначала выбираем в индексе наиболее подходящее слово, чтобы уменьшить перебор
             */
            ArrayList<WordInterface> pat = Word.split(pattern_text);
            for (int i = 0; i < pat.size(); i++) {
                WordInterface w = pat.get(i);
                if (w instanceof WordUnicode) {
                    pat.set(i, new WordIndex((WordUnicode) w, true));
                }
            }

            Collections.sort(pat, indexHeightComparator);
            cursor = index.new LikeIterator(null);
            cursor.patsearch = new PatternSearch((WordIndex) pat.get(0));
            from_store = false;
        }
        ArrayList<Node<RefData>> searchResult = new ArrayList<Node<RefData>>(results_per_page);
        while (cursor.hasNext()) {
            Node<RefData> node = cursor.nextDataNode();
            if (node != null) {
                final byte[] text = join(node.value.value);
                int res = cursor.patsearch.index(text);

                if (res != -1) { // дофильтрация
                    searchResult.add(node);
                    if (searchResult.size() == results_per_page)
                        break;
                }
            }
        }
        if (!from_store && cursor.hasNext()) {
            filterLikeCursor.put(reqid, cursor);
        } else if (from_store) {
            filterLikeCursor.remove(reqid);
        }

        return toJson(searchResult, cursor.hasNext(), null);
    }

    /**
     * Поиск filterLike2 - поиск записей справочнике ФНСИ по Value. Запись выдается
     * поиском если в Value входят искомые подслова (без учета регистра).
     * Поиск морфологический. То есть без учета окончаний существительных и
     * прилагательных.
     * 
     * Алгоритм поиска - по индексы выбираются все записи data, в которые входят
     * слова шаблона.
     * Далее выполняется пересечение этих массивов, что и будет являться
     * результатом.
     * 
     * Пример:
     * Шаблон 'медицинская сес'
     * 
     * Данные:
     * 'медицинская сестра' - соответствует
     * 'сестра медицинская' - соответствует
     * 
     * Пример:
     * Шаблон 'едицинск'
     * 
     * Данные:
     * 'медицинская сестра' - не соответствует
     * 
     * Стурктура дерева index оптимизирована под этот вид поиска
     * 
     * @id - идентфикатор запроса. Каждый запрос должен иметь уникальный
     *     идентификатор, который надо сформировать из URL запроса
     * 
     * @pattern - шаблон поиска
     * 
     * @results_per_page - сколько записей вернуть
     */
    @SuppressWarnings("unchecked")
    public JSONObject filterLike2(final String reqid, final String pattern_text, final int results_per_page)
            throws IOException {
        if (isEmpty() || !filterLike2Ready) {
            return toJson(EMPTY_RESULT, false, "Data not ready");
        }

        ArrayList<WordInterface> pat = Word.split(pattern_text);
        AVLTree<Iterable<Node<RefData>>>.LikeIterator[] cursors = filterLike2Cursors.get(reqid);
        boolean from_store = true;
        int cursor_i = 0; // текущий курсор. Если i<0, то пересечений нет.
        if (cursors == null) {
            int c = 0;
            for (int i = 0; i < pat.size(); i++) {
                WordInterface w = pat.get(i);
                if (w instanceof WordUnicode) {
                    c++;
                    pat.set(i, new WordIndex((WordUnicode) w, true));
                }
            }

            Collections.sort(pat, indexHeightComparator);
            cursors = (AVLTree<Iterable<Node<RefData>>>.LikeIterator[]) new AVLTree.LikeIterator[c];
            int i = 0;
            for (WordInterface w : pat) {
                WordIndex p = (WordIndex) w;
                Node<Iterable<Node<RefData>>> subroot = index.getSubtreeByPattern(p);
                if (subroot == null) {
                    return toJson(EMPTY_RESULT, false, null);
                }
                AVLTree<Iterable<Node<RefData>>>.LikeIterator cursor = index.new LikeIterator(subroot);
                cursors[i] = cursor;
                cursors[i++].patsearch = new PatternSearch(p);
            }
            from_store = false;
        } else {
            cursor_i = filterLike2CursorsPosition.get(reqid);
        }

        /*
         * По каждому слову шаблона открывается свой курсор в индексе.
         * Задача - в множестве узлов data у курсоров в index
         * найти все пересечения. Алгоритм поиска пересечений основнован на
         * том, что множество узлов data сортированные.
         * Сортировка производится в функции repackindex.
         */

        // знач предыдущих курсоров. Пересечение найдено, если все курсоры имеют одно
        // знач
        int curid = 0;
        ArrayList<Node<RefData>> searchResult = new ArrayList<Node<RefData>>(results_per_page);
        while (searchResult.size() != results_per_page) {
            AVLTree<Iterable<Node<RefData>>>.LikeIterator cursor = cursors[cursor_i];

            if (cursor_i == 0) {
                curid = 0;
            }

            // в текущем курсоре берем следующий элемент
            Node<RefData> node = null;
            while (cursor.hasNext() && node == null) {
                node = cursor.nextDataNode();
            }
            if (node == null) { // дошли до конца, возвращаемся к предыдущему курсору
                if (cursor_i == 0) {
                    cursor_i = -1;
                    break;
                }
                cursor_i = 0;
                continue;
            }

            // соотносим текущее значение с состоянием предыдущих курсоров
            if (cursor_i == 0) { // самый первый элемент
                curid = node.getId();
                if (cursors.length == 1) {
                    searchResult.add(node);
                } else {
                    cursor_i++;
                }
                continue;
            }
            if (curid == node.getId()) {
                if (cursor_i == cursors.length - 1) {
                    searchResult.add(node);
                    cursor_i = 0;
                } else {
                    cursor_i++;
                }
                continue;
            }
            if (curid < node.getId()) { // пользуемся тем, что отсортировано
                cursor.nextIndexNode();
                curid = 0;
                continue;
            }
        }

        if (!from_store && cursor_i >= 0) {
            filterLike2Cursors.put(reqid, cursors);
            filterLike2CursorsPosition.put(reqid, cursor_i - 1);
        } else if (from_store) {
            filterLike2Cursors.remove(reqid);
            filterLike2CursorsPosition.remove(reqid);
        }

        return toJson(searchResult, (cursor_i >= 0), null);
    }

    /**
     * Сохранить содержимое справочника в JSON файл в собственном формате
     * 
     * @param writer - открытый поток
     * @throws IOException
     */
    public void saveJSON(Writer writer) throws IOException {
        saveDictJSON(writer);
        writer.write("===DATA\n");
        saveDataJSON(writer);
        writer.write("===INDEX\n");
        repackIndex();
        saveIndexJSON(writer);
        filterLike2Ready = true;
    }

    /**
     * Загрузить содержимое справочника из JSON файла в собственном формате
     * 
     * @param reader - открытый поток
     * @throws IOException
     */
    public void loadJSON(final BufferedReader reader) throws IOException {
        TreeJSONReader<WordInterface> parser1 = new TreeJSONReader<WordInterface>(reader);
        dict.load(parser1);
        TreeJSONReader<RefData> parser2 = new TreeJSONReader<RefData>(reader);
        data.load(parser2);
        TreeJSONReader<Iterable<Node<RefData>>> parser3 = new TreeJSONReader<Iterable<Node<RefData>>>(reader);
        index.load(parser3);
        filterLike2Ready = true;
    }

    private void saveDictJSON(Writer writer) throws IOException {
        LinkedList<Object[]> list = new LinkedList<Object[]>();
        AVLTree<WordInterface>.StoreIteratorAbstract cursor = dict.new StoreIteratorAbstract() {
            Object[] storeNode(final Node<WordInterface> node) {
                node.setId(count);
                Object[] jo = new Object[6];
                jo[0] = node.getId();
                jo[1] = null; // left
                jo[2] = null; // right
                if (!stack.isEmpty())
                    jo[3] = stack.lastElement().getId();
                else
                    jo[3] = null;
                jo[4] = node.key.toString();
                jo[5] = node.value.toJSON();

                list.add(jo);
                return jo;
            }
        };

        while (cursor.hasNext()) {
            cursor.next();
        }

        for (Object[] ar : list) {
            JSONArray ja = new JSONArray(ar);
            writer.write(ja.toString());
            writer.write("\n");
        }
    }

    private void saveDataJSON(Writer writer) throws IOException {
        LinkedList<Object[]> list = new LinkedList<Object[]>();
        AVLTree<RefData>.StoreIteratorAbstract cursor = data.new StoreIteratorAbstract() {
            Object[] storeNode(final Node<RefData> node) {
                node.setId(count);
                Object[] jo = new Object[7];
                jo[0] = node.getId();
                if (!stack.isEmpty())
                    jo[3] = stack.lastElement().getId();
                jo[4] = node.key.toString();
                JSONArray ar = new JSONArray();
                for (WordInterface n : node.value.value) {
                    if (n instanceof Word)
                        ar.put(dict.get((Word) n).getId());
                    else
                        ar.put(dict.get(n.toString()).getId());
                }
                jo[5] = ar;
                if (node.value.extdata != null)
                    jo[6] = new JSONArray(node.value.extdata);

                list.add(jo);
                return jo;
            }
        };
        while (cursor.hasNext()) {
            cursor.next();
        }

        for (Object[] ar : list) {
            JSONArray ja = new JSONArray(ar);
            writer.write(ja.toString());
            writer.write("\n");
        }
    }

    private void saveIndexJSON(Writer writer) throws IOException {
        LinkedList<Object[]> list = new LinkedList<Object[]>();
        AVLTree<Iterable<Node<RefData>>>.StoreIteratorAbstract cursor = index.new StoreIteratorAbstract() {
            Object[] storeNode(final Node<Iterable<Node<RefData>>> node) {
                node.setId(count);
                Object[] jo = new Object[6];
                jo[0] = node.getId();
                if (!stack.isEmpty())
                    jo[3] = stack.lastElement().getId();
                jo[4] = node.key.toString();
                LinkedList<Integer> d = new LinkedList<Integer>();
                for (Node<RefData> n : node.value)
                    d.add(n.getId());
                jo[5] = new JSONArray(d);

                list.add(jo);
                return jo;
            }
        };
        while (cursor.hasNext()) {
            cursor.next();
        }

        for (Object[] ar : list) {
            JSONArray ja = new JSONArray(ar);
            writer.write(ja.toString());
            writer.write("\n");
        }
    }

    @SuppressWarnings("unchecked")
    private void saveDictBinary(DataOutputStream dos) throws IOException {
        LinkedList<Object[]> list = new LinkedList<Object[]>();
        AVLTree<WordInterface>.StoreIteratorAbstract cursor = dict.new StoreIteratorAbstract() {
            Object[] storeNode(final Node<WordInterface> node) {
                node.setId(count);
                Object[] bo = new Object[4];
                bo[0] = node;
                bo[1] = 0; // left
                bo[2] = 0; // right
                bo[3] = (!stack.isEmpty()) ? stack.lastElement().getId() : 0; // parent

                list.add(bo);
                return bo;
            }
        };
        while (cursor.hasNext()) {
            cursor.next();
        }

        for (Object[] line : list) {
            Node<WordInterface> node = (Node<WordInterface>) line[0];
            dos.writeInt(node.getId());
            dos.writeInt((Integer) line[1]);
            dos.writeInt((Integer) line[2]);
            dos.writeInt((Integer) line[3]);
            TreeBinaryReaderWriter.writeArray(dos, node.key.value);
            node.value.write(dos);
        }
    }

    @SuppressWarnings("unchecked")
    private void saveDataBinary(DataOutputStream dos) throws IOException {
        LinkedList<Object[]> list = new LinkedList<Object[]>();
        AVLTree<RefData>.StoreIteratorAbstract cursor = data.new StoreIteratorAbstract() {
            Object[] storeNode(final Node<RefData> node) {
                node.setId(count);
                Object[] bo = new Object[5];
                bo[0] = node;
                bo[1] = 0; // left
                bo[2] = 0; // right
                bo[3] = (!stack.isEmpty()) ? stack.lastElement().getId() : 0; // parent
                LinkedList<Object> ar = new LinkedList<Object>();
                for (WordInterface n : node.value.value) {
                    if (n instanceof Word)
                        ar.add(dict.get((Word) n).getId());
                    else
                        ar.add(dict.get(n.toString()).getId());
                }
                bo[4] = ar.toArray(); // value

                list.add(bo);
                return bo;
            }
        };
        while (cursor.hasNext()) {
            cursor.next();
        }

        for (Object[] line : list) {
            NodeDataExt<RefData> node = (NodeDataExt<RefData>) line[0];
            dos.writeInt(node.getId());
            dos.writeInt((Integer) line[1]);
            dos.writeInt((Integer) line[2]);
            dos.writeInt((Integer) line[3]);
            TreeBinaryReaderWriter.writeArray(dos, node.key.value);
            TreeBinaryReaderWriter.writeIntegerArray(dos, (Object[]) line[4]);
            if (node.value.extdata != null) {   // чтение в readStringArrayDelayed
                dos.writeChar(node.value.extdata.length);
                node.pos = dos.size();
                dos.writeInt(0); // pos
                dos.writeInt(0); // len
            } else {
                dos.writeChar(0);
            }
        }
    }

    private void saveExtDataBinary(DataOutputStream dos) throws IOException {
        AVLTree<RefData>.RefIterator cursor = data.new RefIterator();
        while (cursor.hasNext()) {
            NodeDataExt<RefData> node = (NodeDataExt<RefData>) cursor.next();
            if (node.value.extdata != null) {
                node.posExtData = dos.size();
                TreeBinaryReaderWriter.writeStringArray(dos, (String[]) node.value.extdata);
                node.lenExtData = dos.size() - node.posExtData;
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void saveIndexBinary(DataOutputStream dos) throws IOException {
        LinkedList<Object[]> list = new LinkedList<Object[]>();
        AVLTree<Iterable<Node<RefData>>>.StoreIteratorAbstract cursor = index.new StoreIteratorAbstract() {
            Object[] storeNode(final Node<Iterable<Node<RefData>>> node) {
                node.setId(count);
                Object[] bo = new Object[5];
                bo[0] = node;
                bo[1] = 0; // left
                bo[2] = 0; // right
                bo[3] = (!stack.isEmpty()) ? stack.lastElement().getId() : 0; // parent
                LinkedList<Integer> d = new LinkedList<Integer>();
                for (Node<RefData> n : node.value)
                    d.add(n.getId());
                bo[4] = d.toArray(); // value

                list.add(bo);
                return bo;
            }
        };
        while (cursor.hasNext()) {
            cursor.next();
        }

        try {
            for (Object[] line : list) {
                Node<Iterable<Node<RefData>>> node = (Node<Iterable<Node<RefData>>>) line[0];
                dos.writeInt(node.getId());
                dos.writeInt((Integer) line[1]);
                dos.writeInt((Integer) line[2]);
                dos.writeInt((Integer) line[3]);
                TreeBinaryReaderWriter.writeArray(dos, node.key.value);
                TreeBinaryReaderWriter.writeBigIntegerArray(dos, (Object[]) line[4]);
            }
        } catch (IOException ex) {
            System.out.println(ex);
        }
    }

    /**
     * Сохранить содержимое справочника в бинарный файл
     * 
     * @param filename - имя файла
     * @throws IOException
     */
    public void saveBinary(final String filename) throws IOException {
        DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename)));
        saveDictBinary(dos);
        dos.writeInt(0); // delimiter
        saveDataBinary(dos);
        dos.writeInt(0); // delimiter
        repackIndex();
        saveIndexBinary(dos);
        filterLike2Ready = true;
        dos.writeInt(0); // delimiter
        saveExtDataBinary(dos);
        dos.close();

        RandomAccessFile raf = new RandomAccessFile(filename, "rw");
        MappedByteBuffer mbb = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, raf.length());

        AVLTree<RefData>.RefIterator cursor = data.new RefIterator();
        while (cursor.hasNext()) {
            NodeDataExt<RefData> node = (NodeDataExt<RefData>) cursor.next();
            if (node.value.extdata != null) {
                mbb.position(node.pos);
                mbb.putInt(node.posExtData);
                mbb.putInt(node.lenExtData);
            }
        }

        mbb.force();
        raf.close();
    }

    public String toString() {
        return ((codeSystem != null) ? codeSystem : "null") + "-" + ((version != null) ? version : "null");
    }

    /**
     * Загрузить содержимое справочника из бинарного файла
     * 
     * @param filename - имя файла
     * @throws IOException
     */
    public void loadBinary(final String filename) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(filename, "r");
        MappedByteBuffer mbb = raf.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, raf.length());
        TreeBinaryReaderWriter<WordInterface> parser1 = new TreeBinaryReaderWriter<WordInterface>(mbb);
        dict.load(parser1);
        TreeBinaryReaderWriter<RefData> parser2 = new TreeBinaryReaderWriter<RefData>(mbb);
        data.load(parser2);
        IndexTreeBinaryReader<Iterable<Node<RefData>>> parser3 = new IndexTreeBinaryReader<Iterable<Node<RefData>>>(
                mbb);
        index.load(parser3);
        raf.close();
        binfilename = filename;
        filterLike2Ready = true;
    }

    /**
     * Преобразование результата выполнения запроса к БД в JSON в формате ФНСИ
     * Относительно аналогичного ответа сервиса ФНСИ добавлен 3 реквизита со
     * сведения о справочнике,
     * из которого получены запрашивемые сведения.
     */
    private JSONObject toJson(final ArrayList<Node<RefData>> d, final boolean hasNext, final String resultText)
            throws IOException {
        JSONObject ret = new JSONObject();
        ret.put("result", (resultText == null) ? "OK" : "ERROR");
        ret.put("resultText", resultText);
        ret.put("codeSystem", codeSystem);
        ret.put("codeSystemVersion", version);
        ret.put("codeSystemName", codeSystemName);
        ret.put("resultCode", (Object) null);
        ret.put("total", d.size());
        ret.put("hasNext", hasNext);
        JSONArray a = new JSONArray(d.size());
        ret.put("list", a);
        for (Node<RefData> n : d) {
            JSONObject jKey = new JSONObject();
            jKey.put("column", keyField);
            jKey.put("value", n.key.toString());
            a.put(jKey);
            JSONObject jVal = new JSONObject();
            jVal.put("column", valueField);
            jVal.put("value", toString(n.value.value));
            a.put(jVal);
            if (fieldNames != null && n.value.extdata != null) {
                if (n.value.extdata instanceof Integer[]) {
                    int str_len = (Integer) n.value.extdata[0];
                    if (str_len != 0) {
                        int pos = (Integer) n.value.extdata[1];
                        int len = (Integer) n.value.extdata[2];
                        n.value.extdata = TreeBinaryReaderWriter.readStringArray(binfilename, str_len, pos, len);
                    } else {
                        throw new UnsupportedOperationException();
                    }
                }
                for (int i = 0; i < fieldNames.length; i++) {
                    JSONObject jo = new JSONObject();
                    jo.put("column", fieldNames[i]);
                    jo.put("value", n.value.extdata[i]);
                    a.put(jo);
                }
            }
        }
        return ret;
    }

    /**
     * Поле value справочника (массив слов) преобразует в строку в win1251
     * Метод используется для дофильтрации в запросе filterLike
     */
    static byte[] join(final Iterable<WordInterface> nodes) {
        int c = 0;
        for (WordInterface w : nodes) {
            c += w.length();
        }

        final byte[] ret = new byte[c];
        int i = 0;
        for (WordInterface w : nodes) {
            final int wl = w.length();
            if (w instanceof WordUnicode) {
                Word w2 = new Word((WordUnicode) w);
                System.arraycopy(w2.value, 0, ret, i, wl);
            } else if (w instanceof Word) {
                System.arraycopy(((Word) w).value, 0, ret, i, wl);
            } else {
                Arrays.fill(ret, i, i + wl, (byte) 32); // пробел
            }
            i += wl;
        }
        return ret;
    }

    /**
     * Поле value справочника (массив слов) преобразует преобразовать в String
     */
    static String toString(final Iterable<WordInterface> arr) {
        StringBuilder sb = new StringBuilder();
        for (WordInterface n : arr) {
            sb.append(n.toString());
        }
        return sb.toString();
    }
}